/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.events.aggr;

import java.io.Serializable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Transient;
import org.apereo.portal.events.aggr.groups.AggregatedGroupMapping;
import org.apereo.portal.events.aggr.stat.JpaStatisticalSummary;

/** Base for aggregate entities that track timed statistics */
@Access(AccessType.FIELD)
@MappedSuperclass
public abstract class BaseTimedAggregationStatsImpl<
                K extends BaseAggregationKey, D extends BaseGroupedAggregationDiscriminator>
        extends BaseAggregationImpl<K, D> implements TimedAggregationStatistics, Serializable {
    private static final long serialVersionUID = 1L;

    @Column(name = "TIME_COUNT", nullable = false)
    private int count;

    @Column(name = "SUM_TIME", nullable = false)
    private double sum;

    @Column(name = "SUMSQ_TIME", nullable = false)
    private double sumsq;

    @Column(name = "MEAN_TIME", nullable = false)
    private double mean;

    @Column(name = "STD_DEVIATION_TIME", nullable = false)
    private double standardDeviation;

    @Column(name = "VARIANCE_TIME", nullable = false)
    private double variance;

    @Column(name = "POPULATION_VARIANCE_TIME", nullable = false)
    private double populationVariance;

    @Column(name = "MAX_TIME", nullable = false)
    private double max;

    @Column(name = "MIN_TIME", nullable = false)
    private double min;

    @Column(name = "GEOMETRIC_MEAN_TIME", nullable = false)
    private double geometricMean;

    @Column(name = "SUM_OF_LOGS_TIME", nullable = false)
    private double sumOfLogs;

    @Column(name = "SECOND_MOMENT_TIME", nullable = false)
    private double secondMoment;

    @Embedded private JpaStatisticalSummary statisticalSummary;

    @Column(name = "STATS_COMPLETE", nullable = false)
    private boolean complete = false;

    @Transient private boolean modified = false;

    protected BaseTimedAggregationStatsImpl() {
        super();
    }

    protected BaseTimedAggregationStatsImpl(
            TimeDimension timeDimension,
            DateDimension dateDimension,
            AggregationInterval interval,
            AggregatedGroupMapping aggregatedGroup) {
        super(timeDimension, dateDimension, interval, aggregatedGroup);
    }

    @Override
    public final long getN() {
        updateStats();
        return this.count;
    }

    @Override
    public final double getSum() {
        updateStats();
        return this.sum;
    }

    @Override
    public final double getSumsq() {
        updateStats();
        return this.sumsq;
    }

    @Override
    public final double getMean() {
        updateStats();
        return this.mean;
    }

    @Override
    public final double getStandardDeviation() {
        updateStats();
        return this.standardDeviation;
    }

    @Override
    public final double getVariance() {
        updateStats();
        return this.variance;
    }

    @Override
    public final double getPopulationVariance() {
        updateStats();
        return this.populationVariance;
    }

    @Override
    public final double getMax() {
        updateStats();
        return this.max;
    }

    @Override
    public final double getMin() {
        updateStats();
        return this.min;
    }

    @Override
    public final double getGeometricMean() {
        updateStats();
        return this.geometricMean;
    }

    @Override
    public final double getSumOfLogs() {
        updateStats();
        return this.sumOfLogs;
    }

    @Override
    public final double getSecondMoment() {
        updateStats();
        return this.secondMoment;
    }

    /** Check if the interval is complete, must be called by super classes if overridden */
    @Override
    protected boolean isComplete() {
        return this.count > 0 && (this.complete || this.statisticalSummary == null);
    }

    /** Completes the stats interval, must be called by super classes if overridden */
    @Override
    protected void completeInterval() {
        updateStats();

        this.statisticalSummary = null;
        this.complete = true;
    }

    /** Add the value to the summary statistics */
    public final void addValue(double v) {
        if (isComplete()) {
            this.getLogger()
                    .warn(
                            "{} is already closed, the new value of {} will be ignored on: {}",
                            this.getClass().getSimpleName(),
                            v,
                            this);
            return;
        }

        // Lazily init the statistics object
        if (this.statisticalSummary == null) {
            this.statisticalSummary = new JpaStatisticalSummary();
        }

        this.statisticalSummary.addValue(v);

        this.modified = true;
    }

    /**
     * Update the individual statistic fields if the {@link JpaStatisticalSummary} has been
     * modified, called automatically by the getter of each field
     */
    @PrePersist
    @PreUpdate
    final void updateStats() {
        if (!this.modified || this.statisticalSummary == null) {
            return;
        }

        // Update statistic values
        this.count = (int) this.statisticalSummary.getN();
        this.sum = this.statisticalSummary.getSum();
        this.sumsq = this.statisticalSummary.getSumsq();
        this.mean = this.statisticalSummary.getMean();
        this.standardDeviation = this.statisticalSummary.getStandardDeviation();
        this.variance = this.statisticalSummary.getVariance();
        this.populationVariance = this.statisticalSummary.getPopulationVariance();
        this.max = this.statisticalSummary.getMax();
        this.min = this.statisticalSummary.getMin();
        this.geometricMean = this.statisticalSummary.getGeometricMean();
        this.sumOfLogs = this.statisticalSummary.getSumOfLogs();
        this.secondMoment = this.statisticalSummary.getSecondMoment();

        this.modified = false;
    }
}
